Socks5简介
Socks5是一个代理协议,在客户端和服务器之间扮演中间角色,支持TCP/UDP传输协议。 有以下功能和特点
- 支持IPv4和IPv6
- 用户验证
- 数据加密
- UPD转发
协议的交互过程
- PC发起访问服务器的请求
- Socks5客户端拦截请求,Socks5客户端主动跟Socks5代理服务器建立TCP连接。
- Socks5客户端跟代理服务器认证
- 认证通过后,Socks5代理服务器与服务器建立连接
- 请求建立连接后Socks5代理服务器与服务器进行数据交互
- Socks5代理服务器把数据通过socket转发给Socks5客户端
- 客户端将数据转发给PC
应用场景
- 绕过网络封锁
- 加速网络连接
- 绕过地理限制
- 企业内部网络
实战
搭建TCP echo server
TCP echo server 是一种基于TCP协议的服务器程序,主要功能是接受客户端发送的数据,并将数据原样返回给客户端,可以方便的检查客户端和服务端之间的通信是否正常.
创建一个网络端口
server,err :=net.Listen("tcp","127.0.0.1:1080")
if err !=nil{
panic(err)
}
panic()函数
panic()是Go语言中用于引发程序崩溃的函数,调用的好死后会导致程序正常的控制流程中断,立即开始执行清理工作(调用defer语句),然后退出当前的协程最终退出整个程序
恢复panic(recover()) 在一些情况中可以使用recover()函数从panic中恢复,通常在defer中使用recover()来捕获panic,使得程序在发生错误后继续执行或执行一些必要的清理工作
defer func()
{
if r:=recover();r!nil{
fmt.Println("Recovered from panic",r)
}
}()
// 在下面写panic,程序遇到崩溃的时候会调用recover,
//捕捉到错误信息恢复程序运行,不影响之后的代码
调用服务器连接并接受请求
在大多数服务器程序的基本设计中需要用到死循环来保证服务器能够
- 持续监听和接受客户端连接
- 并发处理多个客户端的请求,避免阻塞
- 保持稳定运行,而不是在处理一个连接后退出
于是代码如下
for {
// 这里用server.Accept()去接受一个请求,如果成功的话会返回一个连接
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
这里的go会启动一个协程(goroutine)这个可以类比成子线程,但是这个比子线程占用要小 使得这个process函数在一个新的协程中执行,而不是主协程,能够让程序在并发环境中执行多个任务,这样服务器就能够同时服务多个客户端,从而实现并发处理大量客户端的需求
将请求中的数据读取
func process(conn net.Conn) {
// 这一行代码的意思是,在这个函数结束的时候,要把这个连接关闭
// 使得这个连接的生命周期就是这个函数的生命周期
defer conn.Close()
reader := bufio.NewReader(conn)
for {
// 用reader.ReadByte使得我们每次都读取一个字节
b, err := reader.ReadByte()
if err != nil {
break
}
// 然后用conn.Write来把字节写入
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
bufio.NewReader()
这里调用bufio.NewReader创建一个只读带缓冲的流,这样比直接读取更加高效,能够减少读取次数减少对底层资源的调用,把数据批量的读取到缓存区,然后逐行,逐字节或按照固定大小提供,提高性能 这里代码中可能以为是一个一个字节读入会很慢,实际上在读第一个字节的时候就可能把后面1k的已经预读取了,后面调用就很快了
完整代码
package main
import (
"bufio"
"log"
"net"
)
// TCP echo server 是一种基于TCP协议的服务器程序,主要功能是接受客户端发送的数据,并将数据原样返回给客户端,
// 可以方便的检查客户端和服务端之间的通信是否正常
func main() {
// 用net.Listen来增加一个端口
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
// 这里用server.Accept()去接受一个请求,如果成功的话会返回一个连接
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
// 然后调用这个process函数去处理这个连接
// 这里的go会启动一个协程(goroutine)这个可以类比成子线程,但是这个比子线程占用的要小
// 这样使得这个process函数在一个新的协程中执行,而不是主协程。
// 这种方法可以让程序在并发环境中异步执行多个任务
// 这样服务器就能同时服务多个客户端,从而实现并发处理大量客户端的需求
go process(client)
}
}
func process(conn net.Conn) {
// 这一行代码的意思是,在这个函数结束的时候,要把这个连接关闭
// 使得这个连接的生命周期就是这个函数的生命周期
defer conn.Close()
// 用bufio.NewReader创建一个只读带缓冲的的流,比直接读取更加高效
// 减少读取次数减少对底层资源的调用,把数据批量的读取到缓存区
// 然后逐行,逐字节或按照固定大小提供,提高性能
reader := bufio.NewReader(conn)
for {
// 用reader.ReadByte使得我们每次都读取一个字节
b, err := reader.ReadByte()
if err != nil {
break
}
// 然后用conn.Write来把字节写入
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
启动
先打开一个终端指定到这个文件目录中
/goLearnProject$ go run tcpEchoServer.go
run后面的是你的文件名
然后打开另外一个终端同样指定到这个文件目录
/goLearnProject$ nc 127.0.0.1 1080
接下来在这个终端输入你想要发送的内容就可以了
他会自动返回你刚刚输入的内容
实现协议认证阶段
相关定义
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const stypeIPV6 = 0x04
// +----+----------+----------+
// |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
修改process方法
我们刚刚的process方法是为了测试服务器与客户端之间的连接,现在要用来测试认证。 代码如下
func process(conn net.Conn) {
// 这一行代码的意思是,在这个函数结束的时候,要把这个连接关闭
// 使得这个连接的生命周期就是这个函数的生命周期
defer conn.Close()
// 用bufio.NewReader创建一个只读带缓冲的的流,比直接读取更加高效
// 减少读取次数减少对底层资源的调用,把数据批量的读取到缓存区
// 然后逐行,逐字节或按照固定大小提供,提高性能
reader := bufio.NewReader(conn)
// 调用 auth 函数执行 SOCKS5 的认证
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
log.Println("auth success")
}
这里调用auth函数来处理认证,进行数据字段的检测
auth方法
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 err:%v", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
// 这里的methodSize其实就是读取的NMETHODS
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read NMETHODS err:%v", err)
}
// 根据这个methodSize,提前开好相应字节的空间
method := make([]byte, methodSize)
// 然后根据空间批量读取数据
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read methods err:%v", err)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
//返回 SOCKS5 服务器的响应,其中 0x00 表示不需要认证。
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
因为ver协议版本和NMETHODS都是一个字节的,直接调用reader.ReadByte()就可以了 然后METHODS需要根据NMETHODS决定的,所以我们用NMETHODS给METHODS开个对应的空间,然后再根据这个长度将数据读入进去就好了。
最后返回Socks5服务器响应的代码,是用了Write把[0x05,0x00]这两个字节发送给客户端,告诉客户端认证成功,可以继续后续的请求
完整代码
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
)
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const stypeIPV6 = 0x04
// TCP echo server 是一种基于TCP协议的服务器程序,主要功能是接受客户端发送的数据,并将数据原样返回给客户端,
// 可以方便的检查客户端和服务端之间的通信是否正常
func main() {
// 用net.Listen来增加一个端口
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
// 这里用server.Accept()去接受一个请求,如果成功的话会返回一个连接
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
// 然后调用这个process函数去处理这个连接
// 这里的go会启动一个协程(goroutine)这个可以类比成子线程,但是这个比子线程占用的要小
// 这样使得这个process函数在一个新的协程中执行,而不是主协程。
// 这种方法可以让程序在并发环境中异步执行多个任务
// 这样服务器就能同时服务多个客户端,从而实现并发处理大量客户端的需求
go process(client)
}
}
func process(conn net.Conn) {
// 这一行代码的意思是,在这个函数结束的时候,要把这个连接关闭
// 使得这个连接的生命周期就是这个函数的生命周期
defer conn.Close()
// 用bufio.NewReader创建一个只读带缓冲的的流,比直接读取更加高效
// 减少读取次数减少对底层资源的调用,把数据批量的读取到缓存区
// 然后逐行,逐字节或按照固定大小提供,提高性能
reader := bufio.NewReader(conn)
// 调用 auth 函数执行 SOCKS5 的认证
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 err:%v", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
// 这里的methodSize其实就是读取的NMETHODS
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read NMETHODS err:%v", err)
}
// 根据这个methodSize,提前开好相应字节的空间
method := make([]byte, methodSize)
// 然后根据空间批量读取数据
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read methods err:%v", err)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
//返回 SOCKS5 服务器的响应,其中 0x00 表示不需要认证。
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
启动
先在一个终端中定位到项目启动go文件
$ go run tcpEchoServer.go
然后打开另外一个终端定位到项目运行一下内容
$ curl --socks5 127.0.0.1:1080 -v http://www.qq.com
这条命令用curl通过本地运行在127.0.0.1:1080的SOCKS5代理服务器,访问qq.com,并返回响应
最终会在第一个终端中打印出
2024/11/09 20:08:48 ver 5 method [0 1]
2024/11/09 20:08:48 auth success
第二个终端中打印
* Trying 127.0.0.1:1080...
* Connected to 127.0.0.1 (127.0.0.1) port 1080
* Host www.qq.com:80 was resolved.
* IPv6: 240e:97c:2f:2::4c, 240e:97c:2f:1::5c
* IPv4: 121.14.77.221, 121.14.77.201
* SOCKS5 connect to [240e:97c:2f:2::4c]:80 (locally resolved)
* connection to proxy closed
* Closing connection
curl: (97) connection to proxy closed
请求阶段,connect方法
修改process方法
将以下代码删除
log.Println("auth success")
添加以下的代码
// 这里是建立连接阶段,复用上面的err可以减少不必要的变量声明
err = connect(reader, conn)
if err != nil {
log.Printf("client %v connect failed:%v", conn.RemoteAddr(), err)
return
}
用于调用connect方法,这里的err复用就可以了,减少不必要的变量声明
connect方法的实现
原理:我们需要解析发送过来的报头
// +----+-----+-------+------+----------+----------+
// |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个字节
这个报头会在认证成功后发送过来的,我们需要去接受、分析,最后返回一个报头
// +----+-----+-------+------+----------+----------+
// |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
以下是connect方法
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,RSV,ATYP刚好是四个字节,我们就直接读入
// 这里的保留字段不管也是一个字节
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 support cmd:%v", cmd)
// 在这里的cmd要等于1才是表示connect请求
}
addr := ""
// 先开一个变量用来存目标地址值
switch atyp {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read addr 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("not support atyp")
default:
return errors.New("invalid atyp")
}
// 接下来读DST.PORT,是两个字节
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])
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)
}
return nil
}
binary.BigEndian用于以 大端字节序 的方式解析数据 Uint16:这是 binary.BigEndian 提供的一个方法,用于将字节切片转换为无符号 16 位整数。 注释已经很详细了就不过多赘述了
relay阶段,实现客户端和目标服务端的通信
尝试建立一个到目标地址的TCP连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
// addr是目标服务
//的地址,port是目标服务器的端口
// 尝试建立一个到目标地址的 TCP 连接。
if err != nil {
return fmt.Errorf("connect dest failed:%w", err)
}
defer dest.Close()
最后进行数据的通信(写入)
ctx, cancel := context.WithCancel(context.Background())
// 创建一个可取消的上下文 (context) 和对应的取消函数 (cancel)。
defer cancel()
go func() {
// 启动一个协程,将 reader 的数据复制到 dest,完成或出错后调用 cancel()。
_, _ = io.Copy(dest, reader)
// 阻塞操作,直到所有数据传输完成或发生错误。
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
这里为什么要用这个context.WithCancel:
-
保证双向数据传输的完整性:
-
ctx 和 cancel 机制确保只有当两个方向的传输都完成时,才退出主函数。 处理异常情况:
-
如果任何一方传输发生错误(如连接断开),另一个方向的传输也会被通知停止。 避免资源泄漏:
-
如果不使用 context.WithCancel,可能出现一个方向的传输完成,而另一个方向仍在无限等待的情况,从而导致协程无法退出。
完整代码
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
// TCP echo server 是一种基于TCP协议的服务器程序,主要功能是接受客户端发送的数据,并将数据原样返回给客户端,
// 可以方便的检查客户端和服务端之间的通信是否正常
func main() {
// 用net.Listen来增加一个端口
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
// 这里用server.Accept()去接受一个请求,如果成功的话会返回一个连接
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
// 然后调用这个process函数去处理这个连接
// 这里的go会启动一个协程(goroutine)这个可以类比成子线程,但是这个比子线程占用的要小
// 这样使得这个process函数在一个新的协程中执行,而不是主协程。
// 这种方法可以让程序在并发环境中异步执行多个任务
// 这样服务器就能同时服务多个客户端,从而实现并发处理大量客户端的需求
go process(client)
}
}
func process(conn net.Conn) {
// 这一行代码的意思是,在这个函数结束的时候,要把这个连接关闭
// 使得这个连接的生命周期就是这个函数的生命周期
defer conn.Close()
// 用bufio.NewReader创建一个只读带缓冲的的流,比直接读取更加高效
// 减少读取次数减少对底层资源的调用,把数据批量的读取到缓存区
// 然后逐行,逐字节或按照固定大小提供,提高性能
reader := bufio.NewReader(conn)
// 调用 auth 函数执行 SOCKS5 的认证
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
// conn.RemoteAddr() 的作用是提供客户端的地址信息
return
}
// 这里是建立连接阶段,复用上面的err可以减少不必要的变量声明
err = connect(reader, conn)
if err != nil {
log.Printf("client %v connect 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 err:%v", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
// 这里的methodSize其实就是读取的NMETHODS
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read NMETHODS err:%v", err)
}
// 根据这个methodSize,提前开好相应字节的空间
method := make([]byte, methodSize)
// 然后根据空间批量读取数据
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read methods err:%v", err)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
//返回 SOCKS5 服务器的响应,其中 0x00 表示不需要认证。
_, 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,RSV,ATYP刚好是四个字节,我们就直接读入
// 这里的保留字段不管也是一个字节
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 support cmd:%v", cmd)
// 在这里的cmd要等于1才是表示connect请求
}
addr := ""
// 先开一个变量用来存目标地址值
switch atyp {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read addr 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("not support atyp")
default:
return errors.New("invalid atyp")
}
// 接下来读DST.PORT,是两个字节
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])
// binary.BigEndian用于以 大端字节序 的方式解析数据
// Uint16:这是 binary.BigEndian 提供的一个方法,用于将字节切片转换为无符号 16 位整数。
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
// addr是目标服务
//的地址,port是目标服务器的端口
// 尝试建立一个到目标地址的 TCP 连接。
if err != nil {
return fmt.Errorf("connect dest 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
// 向客户端发送一个 SOCKS5 连接响应
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
ctx, cancel := context.WithCancel(context.Background())
// 创建一个可取消的上下文 (context) 和对应的取消函数 (cancel)。
//保证双向数据传输的完整性:
//
//ctx 和 cancel 机制确保只有当两个方向的传输都完成时,才退出主函数。
//处理异常情况:
//
//如果任何一方传输发生错误(如连接断开),另一个方向的传输也会被通知停止。
//避免资源泄漏:
//
//如果不使用 context.WithCancel,可能出现一个方向的传输完成,而另一个方向仍在无限等待的情况,从而导致协程无法退出。
defer cancel()
go func() {
// 启动一个协程,将 reader 的数据复制到 dest,完成或出错后调用 cancel()。
_, _ = io.Copy(dest, reader)
// 阻塞操作,直到所有数据传输完成或发生错误。
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
最后用两个终端运行就好了
$ go run tcpEchoServer.go
2024/11/16 19:04:39 ver 5 method [0 1]
2024/11/16 19:04:40 dial 109.244.236.76 80
$ curl --socks5 127.0.0.1:1080 -v http://www.qq.com
* Uses proxy env variable no_proxy == 'localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1'
* Trying 127.0.0.1:1080...
* Connected to 127.0.0.1 (127.0.0.1) port 1080
* Host www.qq.com:80 was resolved.
* IPv6: (none)
* IPv4: 109.244.236.76, 109.244.236.65
* SOCKS5 connect to 109.244.236.76:80 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080
> GET / HTTP/1.1
> Host: www.qq.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: stgw
< Date: Sat, 16 Nov 2024 11:04:40 GMT
< Content-Type: text/html
< Content-Length: 137
< Connection: keep-alive
< Location: https://www.qq.com/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>stgw</center>
</body>
</html>
* Connection #0 to host 127.0.0.1 left intact
如果有什么问题欢迎在下方评论区留言讨论