前言
这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记, 第一次课主要讲了GO语言的一些基础知识,我对其中的第三个项目--socks5代理服务器的项目比较感兴趣,想着重点记录一下相关知识
一、SOCKS5代理原理
正常浏览器访问网站如果不经过代理服务器的话,直接和目标服务器通过三次握手连接
如果通过代理服务器,会通过以下四个阶段
-
第一阶段:协商(握手)阶段
- 协议,认证方式等
-
第二阶段:认证阶段(这里没有认证阶段,因为是不加密的)
-
第三阶段:请求阶段
-
第四阶段:relay阶段
二、SOCKS5实现
1、基本服务
由于这个代理服务器实现起来太复杂,所以开始先实现一个简版的,这个server的逻辑就是你给他发送啥他就会回复啥,用来测试server写的对不对
package main
import (
"bufio"
"log"
"net"
)
func main() {
// 创建一个server
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()
// bufio 创建一个只读的带缓冲的流,
reader := bufio.NewReader(conn)
for {
// 每次读一个字节
b, err := reader.ReadByte()
if err != nil {
break
}
_, err = conn.Write([]byte{b})
if err != nil {
break
}
}
}
运行上述代码,这里测试的时候可以用一个nc命令,这个命令可以直接跟对应的服务器建立tcp连接(windows好像没有nc命令,是不是需要安装什么工具?)
$ nc 127.0.0.0 1080
hello
2、认证部分
- process中增加认证部分,主要是读取协议版本,支持认证方法数量等信息
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)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
// 告诉浏览器选择那种鉴权方式, 0x00表示不需要认证
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
3、请求部分
- 与auth部分类似,使用缓冲区读取协议相关数据,根据目标地址类型选择对应的操作
// 请求阶段,把报文中这六个字段都读出来
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的缓冲区,读取前四个字段
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", ver)
}
addr := ""
// 根据目标地址类型选择
switch atyp {
case atypIPV4:
_, 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])
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
}
4、relay部分
-
重点:
-
copy函数是单向的,使用两个协程创建两个不同方向的copy
- io.Copy(dest, reader) 用户浏览器拷贝数据到底层服务器
- io.Copy(conn, dest) 服务器拷贝数据到用户浏览器
-
context.WithCancel
-
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", ver)
}
addr := ""
switch atyp {
case atypIPV4:
_, 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])
// 建立连接
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)
}
// 用WithCancel创建一个context, ctx.Done()等待ctx执行完成,
// 执行时机也就是cancel被调用的时机,就表示任何一个方向copy失败,就返回
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
// copy单向数据,死循环将数据从reader写到dest, 用户浏览器拷贝数据到底层服务器
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
// 跟上面那个反向,服务器拷贝数据到用户浏览器
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
其他:浏览器插件工具 SwitchyOmega 测试代理